package br.com.hs.nfe.common.util.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection;

/**
 * Classe de auxilio do tipo codificação de exportação.
 * @author Ranlive Hrysyk
 */
public class CharsetToolkit {

    private static byte[] UTF8_BOM = {-17, -69, -65};
    private byte[] buffer;
    private Charset defaultCharset;
    private boolean enforce = true;

    /**
     * Construtor informado o array de bytes
     */
    public CharsetToolkit(byte[] buffer) {
        this.buffer = buffer;
        this.defaultCharset = getDefaultSystemCharset();
    }

    /**
     * Construtor da informando o array de bytes e o charset.
     */
    public CharsetToolkit(byte[] buffer, Charset defaultCharset) {
        this.buffer = buffer;
        setDefaultCharset(defaultCharset);
    }

    /**
     * Informar o charset da classe.
     */
    public final void setDefaultCharset(Charset defaultCharset) {
        if (defaultCharset != null) {
            this.defaultCharset = defaultCharset;
        } else {
            this.defaultCharset = getDefaultSystemCharset();
        }
    }

    /**
     * Forçar leitura de 8 bits.
     */
    public void setEnforce8Bit(boolean enforce) {
        this.enforce = enforce;
    }

    /**
     * Retorna se o tipo de leitura é forçada.
     */
    public boolean getEnforce8Bit() {
        return this.enforce;
    }

    /**
     * Retorna o charset padrão.
     */
    public Charset getDefaultCharset() {
        return this.defaultCharset;
    }

    /**
     * Retorna o encoding do charset.
     */
    public Charset guessEncoding() {
        if (hasUTF8Bom(this.buffer)) {
            return Charset.forName("UTF-8");
        }
        if (min1(this.buffer)) {
            return Charset.forName("UTF-16LE");
        }
        if (min2(this.buffer)) {
            return Charset.forName("UTF-16BE");
        }
        boolean highOrderBit = false;
        boolean validU8Char = true;

        int length = this.buffer.length;
        int i = 0;
        while (i < length - 6) {
            byte b0 = this.buffer[i];
            byte b1 = this.buffer[(i + 1)];
            byte b2 = this.buffer[(i + 2)];
            byte b3 = this.buffer[(i + 3)];
            byte b4 = this.buffer[(i + 4)];
            byte b5 = this.buffer[(i + 5)];
            if (b0 < 0) {
                highOrderBit = true;

                if (max32(b0)) {
                    if (!max64(b1)) {
                        validU8Char = false;
                    } else {
                        i++;
                    }
                } else if (max16(b0)) {
                    if ((!max64(b1)) || (!max64(b2))) {
                        validU8Char = false;
                    } else {
                        i += 2;
                    }
                } else if (max8(b0)) {
                    if ((!max64(b1)) || (!max64(b2)) || (!max64(b3))) {
                        validU8Char = false;
                    } else {
                        i += 3;
                    }
                } else if (max4(b0)) {
                    if ((!max64(b1)) || (!max64(b2)) || (!max64(b3)) || (!max64(b4))) {
                        validU8Char = false;
                    } else {
                        i += 4;
                    }
                } else if (max2(b0)) {
                    if ((!max64(b1)) || (!max64(b2)) || (!max64(b3)) || (!max64(b4)) || (!max64(b5))) {
                        validU8Char = false;
                    } else {
                        i += 5;
                    }
                } else {
                    validU8Char = false;
                }
            }
            if (!validU8Char) {
                break;
            }
            i++;
        }
        if (!highOrderBit) {
            if (this.enforce) {
                return this.defaultCharset;
            }
            return Charset.forName("US-ASCII");
        }
        if (validU8Char) {
            return Charset.forName("UTF-8");
        }
        return this.defaultCharset;
    }

    /**
     * Obtém um o encoding de um deternimado arquivo.
     */
    public static Charset guessEncoding(File f, int bufferLength) throws FileNotFoundException, IOException {
        FileInputStream fis = new FileInputStream(f);
        byte[] buffer = new byte[bufferLength];
        fis.read(buffer);
        fis.close();
        CharsetToolkit toolkit = new CharsetToolkit(buffer);
        toolkit.setDefaultCharset(Charset.forName("UTF-8"));
        return toolkit.guessEncoding();
    }

    /**
     * Obtém um o encoding de um deternimado arquivo.
     */
    public static Charset guessEncoding(File f, int bufferLength, Charset defaultCharset) throws FileNotFoundException, IOException {
        FileInputStream fis = new FileInputStream(f);
        byte[] buffer = new byte[bufferLength];
        fis.read(buffer);
        fis.close();
        CharsetToolkit toolkit = new CharsetToolkit(buffer);
        toolkit.setDefaultCharset(defaultCharset);
        return toolkit.guessEncoding();
    }

    private static boolean max64(byte b) {
        return (-128 <= b) && (b <= -65);
    }

    private static boolean max32(byte b) {
        return (-64 <= b) && (b <= -33);
    }

    private static boolean max16(byte b) {
        return (-32 <= b) && (b <= -17);
    }

    private static boolean max8(byte b) {
        return (-16 <= b) && (b <= -9);
    }

    private static boolean max4(byte b) {
        return (-8 <= b) && (b <= -5);
    }

    private static boolean max2(byte b) {
        return (-4 <= b) && (b <= -3);
    }

    /**
     * Obtém o charset defauld o sistema.
     */
    public static Charset getDefaultSystemCharset() {
        return Charset.forName(System.getProperty("file.encoding"));
    }

    /**
     * Retorna se um array de bytes é UTF-8.
     */
    public static boolean hasUTF8Bom(byte[] bom) {
        return (bom[0] == UTF8_BOM[0]) && (bom[1] == UTF8_BOM[1]) && (bom[2] == UTF8_BOM[2]);
    }

    private static boolean min1(byte[] bom) {
        return (bom[0] == -1) && (bom[1] == -2);
    }

    private static boolean min2(byte[] bom) {
        return (bom[0] == -2) && (bom[1] == -1);
    }

    /**
     * Obtém os charset disponíveis.
     */
    public static Charset[] getAvailableCharsets() {
        Collection collection = Charset.availableCharsets().values();
        return (Charset[]) (Charset[]) collection.toArray(new Charset[collection.size()]);
    }
}
